{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit VellemanManager;

interface

uses Classes, VellemanInputs, Mediate_TLB;

const
  FILE_VELLEMAN = 'VellemanConfiguration.xml';
  DEFAULT_DEBOUNCE_MS = 2;
  DEFAULT_CARD_ADDRESS = 0;
  DEFAULT_ANALOG_OUT_VALUE = 255;
  THREAD_TIMEOUT = 50; // delay for polling thread sync.

type
  TAHMVellemanManager = class;

  TAHMVellemanPollingThread = class(TThread)
  private
    FOwner: TAHMVellemanManager;
  protected
  public
    constructor Create(Owner: TAHMVellemanManager);
    procedure Execute; override;
  end;

  TAHMVellemanManager = class(TPersistent)
  private
    FFilename: String;
    FMediate: IMediateServer;
    FInputs: TAHMVellemanInputs;
    FThread: TAHMVellemanPollingThread;
    FConnected: Boolean;
    FPollingEnabled: Boolean;
    FTimestamp: LongWord;
    FAddress: Integer;
    FDefaultAddress: Integer;
    FDebounceChan1: Integer;
    FDebounceChan2: Integer;
    FAutoAnalogOut1: Boolean;
    FAutoAnalogOut2: Boolean;
    FAutoAnalogVal1: Integer;
    FAutoAnalogVal2: Integer;
    FSaveInputDigital: Integer;
    FSaveInputAnalog1: Integer;
    FSaveInputAnalog2: Integer;
    FSaveInputCounter1: Integer;
    FSaveInputCounter2: Integer;
  protected
    procedure LogMessage(Level: LoggingLevel; LogMessage: String);
    procedure ClearMetaData(Key: String);
    procedure SetMetaData(Key: String; Value: Integer);
    function GetMetaData(Key: String): Integer;
    procedure SetPollingEnabled(Enabled: Boolean);
  public
    constructor Create(Server: IMediateServer);
    destructor Destroy; override;
    property Timestamp: LongWord read FTimestamp;
    function Connect: Boolean; overload;
    function Connect(CardAddress: Integer): Boolean; overload;
    procedure Disconnect;
    procedure Reconnect;
    procedure UpdateAnalogInputs;
    procedure UpdateDigitalInputs;
    procedure UpdateCounterInputs;
    procedure UpdateAllInputs;
    procedure InitialiseAnalogOutputs;
    procedure WriteAnalogOutputs;
    procedure WriteDigitalOutputValue;
    procedure WriteDigitalOutputBits;
    procedure WriteAllOutputs;
    procedure ResetCounter1;
    procedure ResetCounter2;
    procedure ResetCounters;
    procedure Configure;
    procedure PollInputs;
    procedure TriggerAction(ActionName: String);
  published
    property Inputs: TAHMVellemanInputs read FInputs;
    property DefaultAddress: Integer read FDefaultAddress write FDefaultAddress;
    property DebounceChannel1: Integer read FDebounceChan1 write FDebounceChan1;
    property DebounceChannel2: Integer read FDebounceChan2 write FDebounceChan2;
    property AutoAnalogOutput1: Boolean read FAutoAnalogOut1 write FAutoAnalogOut1;
    property AutoAnalogOutput2: Boolean read FAutoAnalogOut2 write FAutoAnalogOut2;
    property AutoAnalogValue1: Integer read FAutoAnalogVal1 write FAutoAnalogVal1;
    property AutoAnalogValue2: Integer read FAutoAnalogVal2 write FAutoAnalogVal2;
    property SavedDigitalInput: Integer read FSaveInputDigital write FSaveInputDigital;
    property SavedAnalog1Input: Integer read FSaveInputAnalog1 write FSaveInputAnalog1;
    property SavedAnalog2Input: Integer read FSaveInputAnalog2 write FSaveInputAnalog2;
    property SavedCounter1Input: Integer read FSaveInputCounter1 write FSaveInputCounter1;
    property SavedCounter2Input: Integer read FSaveInputCounter2 write FSaveInputCounter2;
  end;

implementation

uses SysUtils, MMSystem, PersistenceHelper, VellemanConsts, MetaConsts,
     ConfigurationForm;

const
  VELLEMAN_DLL = 'K8055D.DLL';

// Velleman DLL function mappings
function OpenDevice(CardAddress: Longint): Longint; stdcall; external VELLEMAN_DLL;
procedure CloseDevice; stdcall; external VELLEMAN_DLL;
function ReadAnalogChannel(Channel: Longint):Longint; stdcall; external VELLEMAN_DLL;
procedure ReadAllAnalog(var Data1, Data2: Longint); stdcall; external VELLEMAN_DLL;
procedure OutputAnalogChannel(Channel: Longint; Data: Longint); stdcall; external VELLEMAN_DLL;
procedure OutputAllAnalog(Data1: Longint; Data2: Longint); stdcall; external VELLEMAN_DLL;
procedure ClearAnalogChannel(Channel: Longint); stdcall; external VELLEMAN_DLL;
procedure ClearAllAnalog; stdcall; external VELLEMAN_DLL;
procedure SetAnalogChannel(Channel: Longint); stdcall; external VELLEMAN_DLL;
procedure SetAllAnalog; stdcall; external VELLEMAN_DLL;
procedure WriteAllDigital(Data: Longint); stdcall; external VELLEMAN_DLL;
procedure ClearDigitalChannel(Channel: Longint); stdcall; external VELLEMAN_DLL;
procedure ClearAllDigital; stdcall; external VELLEMAN_DLL;
procedure SetDigitalChannel(Channel: Longint); stdcall; external VELLEMAN_DLL;
procedure SetAllDigital; stdcall; external VELLEMAN_DLL;
function ReadDigitalChannel(Channel: Longint): Boolean; stdcall; external VELLEMAN_DLL;
function ReadAllDigital: Longint; stdcall; external VELLEMAN_DLL;
function ReadCounter(CounterNr: Longint): Longint; stdcall; external VELLEMAN_DLL;
procedure ResetCounter(CounterNr: Longint); stdcall; external VELLEMAN_DLL;
procedure SetCounterDebounceTime(CounterNr, DebounceTime:Longint); stdcall; external VELLEMAN_DLL;

// TAHMVellemanPollingThread

constructor TAHMVellemanPollingThread.Create(Owner: TAHMVellemanManager);
begin
  // Create thread in suspended state
  inherited Create(True);

  // Store owner property for notifications later
  FOwner := Owner;

  // Okay, resume polling thread now
  Resume;
end;

procedure TAHMVellemanPollingThread.Execute;
var
  Delay: Integer;
begin
  while not Terminated do
  begin
    // Get polling period from inputs class
    Delay := FOwner.Inputs.PollingInterval;

    // Sleep in short intervals to avoid blocking on termination
    while (Delay > THREAD_TIMEOUT) and not Terminated do
    begin
      Sleep(THREAD_TIMEOUT);
      Dec(Delay, THREAD_TIMEOUT);
    end;

    // Wake up manager to perform polling
    if not Terminated then Sleep(Delay);
    if not Terminated then FOwner.InitialiseAnalogOutputs;
    if not Terminated then FOwner.PollInputs;
  end;
end;

// TAHMVellemanManager

constructor TAHMVellemanManager.Create(Server: IMediateServer);
var
  ConfigDir: WideString;
begin
  inherited Create;

  // Create input wrapper class
  FInputs := TAHMVellemanInputs.Create(Self);

  // Store server instance & build configuration filename
  FMediate := Server;
  Server.GetConfigDirectory(ConfigDir);
  FFilename := ConfigDir + FILE_VELLEMAN;

  // Populate default configuration settings
  FDefaultAddress := DEFAULT_CARD_ADDRESS;
  FDebounceChan1  := DEFAULT_DEBOUNCE_MS;
  FDebounceChan2  := DEFAULT_DEBOUNCE_MS;
  FAutoAnalogVal1 := DEFAULT_ANALOG_OUT_VALUE;
  FAutoAnalogVal2 := DEFAULT_ANALOG_OUT_VALUE;

  // Load configuration settings & populate default digital inputs
  TAHMPersistenceHelper.LoadObject(Self, FFilename);
  FInputs.DigitalInputs.PopulateDefaults;

  LogMessage(llDebug, 'loaded plugin successfully');
end;

destructor TAHMVellemanManager.Destroy;
begin
  Disconnect;

  // Save configuration settings & current values
  TAHMPersistenceHelper.SaveObject(Self, FFilename);

  // Free input wrapper class
  FInputs.Free;

  LogMessage(llDebug, 'unloaded plugin successfully');

  inherited;
end;

procedure TAHMVellemanManager.LogMessage(Level: LoggingLevel; LogMessage: String);
begin
  FMediate.LogMessage(Level, LogMessage);
end;

procedure TAHMVellemanManager.ClearMetaData(Key: String);
begin
  FMediate.ClearMetaData(Key);
end;

procedure TAHMVellemanManager.SetMetaData(Key: String; Value: Integer);
begin
  FMediate.SetMetaData(Key, IntToStr(Value));
end;

function TAHMVellemanManager.GetMetaData(Key: String): Integer;
var
  Value: WideString;
begin
  FMediate.GetMetaData(Key, Value);
  Result := StrToIntDef(Value, 0);
end;

procedure TAHMVellemanManager.SetPollingEnabled(Enabled: Boolean);
begin
  if Enabled <> FPollingEnabled then
  begin
    // Create/terminate polling thread as required
    if Enabled then
      FThread := TAHMVellemanPollingThread.Create(Self)
    else
    begin
      // Terminate thread and wait for it to stop before freeing
      FThread.Terminate;
      FThread.WaitFor;
      FreeAndNil(FThread);
    end;

    FPollingEnabled := Enabled;
  end;
end;

function TAHMVellemanManager.Connect: Boolean;
begin
  // Connect to current card address
  Result := Connect(FAddress);
end;

function TAHMVellemanManager.Connect(CardAddress: Integer): Boolean;
var
  hr: Longint;
begin
  // Set default result - connection succeeded
  Result := True;

  // Skip if already connected to requested card
  if FConnected and (FAddress = CardAddress) then Exit;

  // OpenDevice returns id of opened card or -1 for error
  hr := OpenDevice(CardAddress);
  if hr in [0..3] then FAddress := hr else Result := False;
  FConnected := Result;

  if FConnected then
  begin
    // Initialise counter debounce times
    SetCounterDebounceTime(1, FDebounceChan1);
    SetCounterDebounceTime(2, FDebounceChan2);

    // Initialise analog output values on connection
    InitialiseAnalogOutputs;

    // Enable input polling thread & live action generation
    SetPollingEnabled(True);
    FInputs.ActionsEnabled := True;
  end;
end;

procedure TAHMVellemanManager.Disconnect;
begin
  if FConnected then
  begin
    // Disable input polling thread
    FInputs.ActionsEnabled := False;
    SetPollingEnabled(False);

    CloseDevice;
    FConnected := False;
  end;
end;

procedure TAHMVellemanManager.Reconnect;
begin
  Disconnect;
  Connect;
end;

procedure TAHMVellemanManager.UpdateAnalogInputs;
var
  Value1, Value2: LongInt;
begin
  // If not connected then clear any current values
  if not FConnected then
    ClearMetaData(META_VELLEMAN_IN_ANALOG)
  else
  begin
    // Get current analog inputs
    ReadAllAnalog(Value1, Value2);
    SetMetaData(META_VELLEMAN_IN_ANACH1, Value1);
    SetMetaData(META_VELLEMAN_IN_ANACH2, Value2);

    // Generate event on analog input change
    if (FSaveInputAnalog1 <> Value1) or (FSaveInputAnalog1 <> Value2) then
      FMediate.TriggerAction(EVENT_VELLEMAN_INPUT_CHANGE_ANALOG);
    FSaveInputAnalog1 := Value1;
    FSaveInputAnalog2 := Value2;
  end;
end;

procedure TAHMVellemanManager.UpdateDigitalInputs;
var
  Value: LongInt;
begin
  // If not connected then clear any current values
  if not FConnected then
    ClearMetaData(META_VELLEMAN_IN_DIGITAL)
  else
  begin
    // Get current digital inputs
    Value := ReadAllDigital;
    SetMetaData(META_VELLEMAN_IN_DIGVAL, Value);
    SetMetaData(META_VELLEMAN_IN_DIGCH1, Value and 1);
    SetMetaData(META_VELLEMAN_IN_DIGCH2, Value shr 1 and 1);
    SetMetaData(META_VELLEMAN_IN_DIGCH3, Value shr 2 and 1);
    SetMetaData(META_VELLEMAN_IN_DIGCH4, Value shr 3 and 1);
    SetMetaData(META_VELLEMAN_IN_DIGCH5, Value shr 4 and 1);

    // Generate event on digital input change
    if FSaveInputDigital <> Value then
      FMediate.TriggerAction(EVENT_VELLEMAN_INPUT_CHANGE_DIGITAL);
    FSaveInputDigital := Value;
  end;
end;

procedure TAHMVellemanManager.UpdateCounterInputs;
var
  Value1, Value2: LongInt;
begin
  // If not connected then clear any current values
  if not FConnected then
    ClearMetaData(META_VELLEMAN_IN_COUNTER)
  else
  begin
    // Get current counter inputs
    Value1 := ReadCounter(1);
    Value2 := ReadCounter(2);
    SetMetaData(META_VELLEMAN_IN_CNTCH1, Value1);
    SetMetaData(META_VELLEMAN_IN_CNTCH2, Value2);

    // Generate event on counter input change
    if (FSaveInputCounter1 <> Value1) or (FSaveInputCounter1 <> Value2) then
      FMediate.TriggerAction(EVENT_VELLEMAN_INPUT_CHANGE_COUNTER);
    FSaveInputCounter1 := Value1;
    FSaveInputCounter2 := Value2;
  end;
end;

procedure TAHMVellemanManager.UpdateAllInputs;
begin
  LogMessage(llDebug, 'updating card input values...');

  UpdateAnalogInputs;
  UpdateDigitalInputs;
  UpdateCounterInputs;

  LogMessage(llDebug, 'finished updating card input values');
end;

procedure TAHMVellemanManager.InitialiseAnalogOutputs;
begin
  // Initialise analog output values (may use a ref. voltages for A-D)
  if FConnected then
  begin
    if FAutoAnalogOut1 then OutputAnalogChannel(1, FAutoAnalogVal1);
    if FAutoAnalogOut2 then OutputAnalogChannel(2, FAutoAnalogVal2);
  end;
end;

procedure TAHMVellemanManager.WriteAnalogOutputs;
var
  Value1, Value2: Integer;
begin
  if FConnected then
  begin
    // Get new analog output values from metadata
    Value1 := GetMetaData(META_VELLEMAN_OUT_ANACH1);
    Value2 := GetMetaData(META_VELLEMAN_OUT_ANACH2);

    OutputAllAnalog(Value1, Value2);
  end;
end;

procedure TAHMVellemanManager.WriteDigitalOutputValue;
var
  Value: Integer;
begin
  if FConnected then
  begin
    // Get new digital output value from metadata
    Value := GetMetaData(META_VELLEMAN_OUT_DIGVAL);

    WriteAllDigital(Value);
  end;
end;

procedure TAHMVellemanManager.WriteDigitalOutputBits;
var
  Value: Integer;
begin
  if FConnected then
  begin
    // Combine bit values to calculate new output value from metadata
    Value := GetMetaData(META_VELLEMAN_OUT_DIGCH1);
    Value := Value + (GetMetaData(META_VELLEMAN_OUT_DIGCH2) * 2);
    Value := Value + (GetMetaData(META_VELLEMAN_OUT_DIGCH3) * 4);
    Value := Value + (GetMetaData(META_VELLEMAN_OUT_DIGCH4) * 8);
    Value := Value + (GetMetaData(META_VELLEMAN_OUT_DIGCH5) * 16);
    Value := Value + (GetMetaData(META_VELLEMAN_OUT_DIGCH6) * 32);
    Value := Value + (GetMetaData(META_VELLEMAN_OUT_DIGCH7) * 64);
    Value := Value + (GetMetaData(META_VELLEMAN_OUT_DIGCH8) * 128);

    WriteAllDigital(Value);
  end;
end;

procedure TAHMVellemanManager.WriteAllOutputs;
begin
  WriteAnalogOutputs;
  WriteDigitalOutputValue;
end;

procedure TAHMVellemanManager.ResetCounter1;
begin
  if FConnected then
  begin
    ResetCounter(1);
  end;
end;

procedure TAHMVellemanManager.ResetCounter2;
begin
  if FConnected then
  begin
    ResetCounter(2);
  end;
end;

procedure TAHMVellemanManager.ResetCounters;
begin
  ResetCounter1;
  ResetCounter2;
end;

procedure TAHMVellemanManager.Configure;
begin
  // Disable action generation whilst configuring (prevents threading issues)
  FInputs.ActionsEnabled := False;
  try
    // Launch configuration form & save changes if required
    if TfrmVellemanConfig.Configure(Self) then
    begin
      LogMessage(llDebug, 'updating velleman settings...');
      TAHMPersistenceHelper.SaveObject(Self, FFilename);
      LogMessage(llDebug, 'velleman settings updated');
    end;
  finally
    FInputs.ActionsEnabled := True;
  end;
end;

procedure TAHMVellemanManager.PollInputs;
var
  Analog1Value, Analog2Value, DigitalValue: LongInt;
begin
  if FConnected then
  begin
    ReadAllAnalog(Analog1Value, Analog2Value);
    DigitalValue := ReadAllDigital;

    // Get current system timestamp for inputs to use
    FTimestamp := timeGetTime;

    // Update inputs with these values
    FInputs.UpdateInputs(Analog1Value, Analog2Value, DigitalValue);
  end;
end;

procedure TAHMVellemanManager.TriggerAction(ActionName: String);
begin
  if ActionName <> '' then
  begin
    LogMessage(llDebug, 'triggering action: ' + ActionName);
    FMediate.TriggerAction(ActionName);
  end;
end;


end.
